{ "cells": [ { "cell_type": "markdown", "metadata": {}, "source": [ "# Principles of Programming Languages" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Blank, Fall 2014" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "# Assignment #6: Implementing Calc in Python with Continuations" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Due: Thursday, October 30, 2014 at beginning of class" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "This assignment is designed to:\n", "- Understand the limits of our language in Python\n", "- Write Python functions in Continuation-Passing Style\n", "- Eliminate the need for a call stack in evaluator" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "From the last couple of assignments, we have created a simple \"Calc\" language that actually looks (and behaves) very similar to Scheme. That can be confusing! Is there anything really special about writing Calc in Scheme? Could we easily implement Calc in another language. Well, let's see!\n", "\n", "Some of you may not know Python, but it is easy to pick up, and may be closer to your first programming language than Scheme is. So, let's re-write what we have so far of our Calc language in Python.\n", "\n", "You may want to search the internet for Pythonisms as we go through this notebook." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Concrete Syntax" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "When we implemented Calc in Scheme, we used Scheme's built-in sexp reader. Thus, we could just represent Calc expressions as quoted Scheme data. That prevented us from using curly braces and commas in our concrete syntax, but got us going quickly.\n", "\n", "Python doesn't have such a built-in reader to read s-expressions. So, let's add just the bare minimum code needed to replicate Scheme's sexp reader. We will write our Calc expressions as a string, and turn that into AST (Abstract Syntax Tree) through a series of parsing steps. \n", "\n", "The first thing we do to make it easy for us is to build a \"lexer\". A lexer just chunks the characters of the given concrete syntax string into parts. So, \"12.3\" will be chunked together, as will \"set!\"." ] }, { "cell_type": "code", "execution_count": null, "metadata": { "collapsed": false }, "outputs": [], "source": [ "def lexer(string):\n", " retval = []\n", " current = \"\"\n", " for i in range(len(string)):\n", " if string[i] in [\"(\", \"[\", \")\", \"]\"]:\n", " if current:\n", " retval.append(current)\n", " current = \"\"\n", " retval.append(string[i])\n", " elif string[i] in [\" \", \"\\t\", \"\\n\"]:\n", " if current:\n", " retval.append(current)\n", " current = \"\"\n", " else:\n", " current += string[i]\n", " if current:\n", " retval.append(current)\n", " return retval" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "collapsed": false }, "outputs": [], "source": [ "lexer(\"(define x (1.2 + 4))\")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "We see that the lexer program is quite simple, chunking based on brackets and white space. Test it to make sure it can handle the Calc items we are interested. Make a note of things it does not correctly handle. We'll need to fix those to make a complete Calc.\n", "\n", "Next, we feed the output of the lexer (a list) into a \"reader\" that turns the \"stream\" of chunks into meaningful items. So, it should turn string numbers into real numbers, and bracketed items into real lists." ] }, { "cell_type": "code", "execution_count": null, "metadata": { "collapsed": false }, "outputs": [], "source": [ "def reader(lexp):\n", " current = None\n", " stack = []\n", " for item in lexp:\n", " if item.isdigit():\n", " if current is not None:\n", " current.append(eval(item))\n", " else:\n", " current = eval(item)\n", " elif item in [\"[\", \"(\"]:\n", " if current is not None:\n", " stack.append(current)\n", " current = []\n", " elif item in [\"]\", \")\"]:\n", " if stack:\n", " stack[-1].append(current)\n", " current = stack.pop(-1)\n", " else:\n", " pass\n", " else:\n", " if current is not None:\n", " current.append(item)\n", " else:\n", " current = item\n", " return current" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "collapsed": false }, "outputs": [], "source": [ "reader(lexer(\"(define x (x + 1))\"))" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Reader is a simple \"finite state machine\" (FSM) that keeps track of being inside of a list of lists, so that it can push and pop off a stack when it encounters nested lists. It is still pretty simple, but this now can be given to a parser much like we did in Scheme. Again, test this to make sure it groups correctly. Make notes of any issues.\n", "\n", "First, let's define some Scheme-like functions to access elements of Python's lists. Notice that the notation list[1:] represents the items in a list starting with position 1 (the second item) and going to the end. Thus, we treat Python's list data structure as if it were a linked-list. It isn't exactly, but we can pretend to make the ideas from our Scheme implementation easy to migrate over to Python." ] }, { "cell_type": "code", "execution_count": null, "metadata": { "collapsed": false }, "outputs": [], "source": [ "def car(exp):\n", " return exp[0]\n", "def cadr(exp):\n", " return exp[1]\n", "def caddr(exp):\n", " return exp[2]\n", "def cadddr(exp):\n", " return exp[3]\n", "\n", "def cdr(exp):\n", " return exp[1:]\n", "def cddr(exp):\n", " return exp[2:]\n", "def cdddr(exp):\n", " return exp[3:]\n", "def cddddr(exp):\n", " return exp[4:]\n", "\n", "def cons(item, lst):\n", " return [item] + lst\n", "\n", "def null_q(lst):\n", " return len(lst) == 0\n", "\n", "def operator_q(item):\n", " return item in [\"+\", \"-\", \"*\", \"/\"]" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Abstract Syntax Tree (AST)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Python doesn't have a `define-datatype`. We could write one. But for now, let's just hard-code the grammar of Calc into these expression functions and the parser:" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "collapsed": false }, "outputs": [], "source": [ "def lit_exp(value):\n", " return [\"lit-exp\", value]\n", "\n", "def if_exp(test_exp, then_exp, else_exp):\n", " return [\"if-exp\", test_exp, then_exp, else_exp]\n", "\n", "def let_exp(variable, value, body):\n", " return [\"let-exp\", variable, value, body]\n", "\n", "def var_exp(symbol):\n", " return [\"var-exp\", symbol]\n", "\n", "def assign_exp(symbol, value):\n", " return [\"assign-exp\", symbol, value]\n", "\n", "def define_exp(symbol, value):\n", " return [\"define-exp\", symbol, value]\n", "\n", "def app_exp(f, args):\n", " return [\"app-exp\", f, args]\n", "\n", "def procedure_exp(variables, body):\n", " return [\"procedure-exp\", variables, body]\n", "\n", "def closure_exp(env, variables, body):\n", " return [\"closure-exp\", env, variables, body]\n", "\n", "## and a utility function for defining closure?:\n", "def closure_q(item):\n", " return (isinstance(item, list) and \n", " len(item) > 0 and\n", " item[0] == \"closure-exp\")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "The parser should look pretty much just like our Scheme version, with the exception that the parenthesis come after the function names in Python. Python doesn't have a `record-case` or even a `case` statement, so we just use a series of if/elif tests." ] }, { "cell_type": "code", "execution_count": null, "metadata": { "collapsed": false }, "outputs": [], "source": [ "def parser(exp):\n", " if isinstance(exp, int):\n", " return lit_exp(exp)\n", " elif isinstance(exp, str):\n", " return var_exp(exp)\n", " elif car(exp) == \"set!\":\n", " return assign_exp(cadr(exp), parser(caddr(exp)))\n", " elif car(exp) == \"define\":\n", " return define_exp(cadr(exp), parser(caddr(exp)))\n", " elif car(exp) == \"if\":\n", " return if_exp(parser(cadr(exp)), parser(caddr(exp)), parser(cadddr(exp)))\n", " elif car(exp) == \"let\":\n", " return let_exp(cadr(exp), parser(caddr(exp)), parser(cadddr(exp)))\n", " elif car(exp) == \"func\":\n", " return procedure_exp(cadr(exp), parser(caddr(exp)))\n", " elif len(exp) == 3 and operator_q(exp[1]):\n", " return app_exp(parser(cadr(exp)), [parser(car(exp)), parser(caddr(exp))])\n", " else:\n", " return app_exp(parser(car(exp)), map(parser, cdr(exp)))" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "collapsed": false }, "outputs": [], "source": [ "parser(reader(lexer(\"23\")))" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "collapsed": false }, "outputs": [], "source": [ "parser(reader(lexer(\"(if 0 23 24)\")))" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "collapsed": false }, "outputs": [], "source": [ "parser(reader(lexer(\"(let x 1 x)\")))" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "collapsed": false }, "outputs": [], "source": [ "parser(reader(lexer(\"(+ 1 2)\")))" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "collapsed": false }, "outputs": [], "source": [ "parser(reader(lexer(\"(let x 1 +)\")))" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "collapsed": false }, "outputs": [], "source": [ "parser(reader(lexer(\"(define x 1)\")))" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "collapsed": false }, "outputs": [], "source": [ "parser(reader(lexer(\"(let x 1 (x + x))\")))" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "collapsed": false }, "outputs": [], "source": [ "parser(reader(lexer(\"(func (x) x)\")))" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "This looks like it is parsing the structures of Calc pretty well. Thoroughly test out as many variations as you can to see if there are any bugs. Note that our parsing code (including lexer and reader) doesn't handle things like quoted items. " ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Evaluation" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Like in Scheme, we need to implement booleans. Again, we use zero for false, and anything for true." ] }, { "cell_type": "code", "execution_count": null, "metadata": { "collapsed": false }, "outputs": [], "source": [ "def true_q(value):\n", " if value == 0:\n", " return False\n", " else:\n", " return True" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Again, we use nested frames of variable/value pairs as the environment. " ] }, { "cell_type": "code", "execution_count": null, "metadata": { "collapsed": false }, "outputs": [], "source": [ "def extend_env(vars, vals, env):\n", " return [map(list,zip(vars, vals)), env]" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "collapsed": false }, "outputs": [], "source": [ "def lookup_binding(symbol, env, first_frame_only=False):\n", " frame = car(env)\n", " for pair in frame:\n", " if pair[0] == symbol:\n", " return pair\n", " if not first_frame_only and cdr(env):\n", " return lookup_binding(symbol, car(cdr(env)))\n", " else:\n", " return False" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Like the Scheme version, we allow functions to be either Python functions or closures:" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "collapsed": false }, "outputs": [], "source": [ "def applier(f, args):\n", " if closure_q(f):\n", " env = cadr(f)\n", " parameters = caddr(f)\n", " body = cadddr(f)\n", " return evaluator(body, extend_env(parameters, args, env))\n", " else:\n", " return apply(f, args)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Finally, the evaluator. It should look completely analogous to the Scheme version:" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "collapsed": false }, "outputs": [], "source": [ "def evaluator(exp, env):\n", " if car(exp) == \"lit-exp\":\n", " return cadr(exp)\n", " elif car(exp) == \"var-exp\":\n", " return cadr(lookup_binding(cadr(exp), env))\n", " elif car(exp) == \"let-exp\":\n", " variable = cadr(exp)\n", " value = evaluator(caddr(exp), env)\n", " return evaluator(cadddr(exp), extend_env([variable], [value], env))\n", " elif car(exp) == \"if-exp\":\n", " if true_q(evaluator(cadr(exp), env)):\n", " return evaluator(caddr(exp), env)\n", " else:\n", " return evaluator(cadddr(exp), env)\n", " elif car(exp) == \"assign-exp\":\n", " variable = cadr(exp)\n", " value = evaluator(caddr(exp), env)\n", " binding = lookup_binding(variable, env)\n", " if binding:\n", " binding[1] = value\n", " else:\n", " raise Exception(\"No such variable: '%s'\" % variable)\n", " return value\n", " elif car(exp) == \"define-exp\":\n", " variable = cadr(exp)\n", " value = evaluator(caddr(exp), env)\n", " binding = lookup_binding(variable, env, True)\n", " if binding:\n", " binding[1] = value\n", " else:\n", " car(env).append([variable, value])\n", " return value\n", " elif car(exp) == \"procedure-exp\":\n", " return closure_exp(env, cadr(exp), caddr(exp))\n", " elif car(exp) == \"app-exp\":\n", " return applier(evaluator(cadr(exp), env), \n", " map(lambda e: evaluator(e, env), caddr(exp)))\n", " else:\n", " raise Exception(\"invalid abstract syntax: %s\" % exp)\n" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Python doesn't allow operators (such as + and -) to be used as \"first-class objects\"---they can't be passed into a function. But, Python supplies equivalent symbols in the \"operator\" library:" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "collapsed": false }, "outputs": [], "source": [ "import operator" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "And so, we define a toplevel environment just like before:" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "collapsed": false }, "outputs": [], "source": [ "toplevel_env = [[[\"+\", operator.add], \n", " [\"-\", operator.sub],\n", " [\"*\", operator.mul],\n", " [\"/\", operator.div],\n", " [\"=\", lambda a,b: 1 if a == b else 0]]]" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Feel free to add more items in the environment.\n", "\n", "And let's test:" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "collapsed": false }, "outputs": [], "source": [ "evaluator(parser(reader(lexer(\"(if 1 23 24)\"))), toplevel_env)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "That is a lot to type to test. Let's make a convenience function, like in Scheme:" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "collapsed": false }, "outputs": [], "source": [ "def calc(exp):\n", " return evaluator(parser(reader(lexer(exp))), toplevel_env)" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "collapsed": false }, "outputs": [], "source": [ "calc(\"(if 0 2 3)\")" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "collapsed": false }, "outputs": [], "source": [ "calc(\"(let x 1 +)\")" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "collapsed": false }, "outputs": [], "source": [ "calc(\"(let x 1 (x + x))\")" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "collapsed": false }, "outputs": [], "source": [ "calc(\"(define y 1)\")" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "collapsed": false }, "outputs": [], "source": [ "toplevel_env" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "collapsed": false }, "outputs": [], "source": [ "calc(\"(set! x 1)\")" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "collapsed": false }, "outputs": [], "source": [ "toplevel_env" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "collapsed": false }, "outputs": [], "source": [ "calc(\"(func (x) x)\")" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "collapsed": false }, "outputs": [], "source": [ "calc(\"((func (x) x) 5)\")" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "collapsed": false }, "outputs": [], "source": [ "calc(\"(define f (let x 1 (func () x)))\")" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "collapsed": false }, "outputs": [], "source": [ "calc(\"(f)\")" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "collapsed": false }, "outputs": [], "source": [ "calc(\"(let x 2 (f))\")" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "collapsed": false }, "outputs": [], "source": [ "calc(\"(define factorial (func (n) (if (= n 1) 1 (* (factorial (- n 1)) n))))\")" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "collapsed": false }, "outputs": [], "source": [ "calc(\"(factorial 5)\")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Excellent! Everything appears to work, just like the Scheme version. Thoroughly test the evaluator to make sure it can handle Calc expressions. Make notes of any failures.\n", "\n", "Finally, let's try one more test, an infinite loop:" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "collapsed": false }, "outputs": [], "source": [ "calc(\"(define loop (func () (loop)))\")" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "collapsed": false }, "outputs": [], "source": [ "calc(\"(loop)\")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Stack overflow!" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "# Fix stack overflow with Continuations" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "To fix our implementation of our Calculator language, we'll need to rewrite the evaluator in Continuation-Passing Style (CPS).\n", "\n", "Let's see what CPS looks like in Python.\n", "\n", "Consider factorial in Python:" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "collapsed": false }, "outputs": [], "source": [ "def factorial(n):\n", " if n == 1:\n", " return 1\n", " else:\n", " return n * factorial(n - 1)" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "collapsed": false }, "outputs": [], "source": [ "factorial(5)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "But even this simple program has the stack overflow issue:" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "collapsed": false }, "outputs": [], "source": [ "factorial(1000)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Stack overflow!" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "We can begin to address the issue by first converting to CPS:" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "collapsed": false }, "outputs": [], "source": [ "def factorial_cps(n, k): ## add extra param, k for continuation\n", " if n == 1:\n", " return k(1) ## pass results to the continuation\n", " else:\n", " return factorial_cps(n - 1, ## move computation after call\n", " lambda v: k(v * n)) ## to a new continuation passed in" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Yes, Python has a `lambda`. Unfortunately, Python's lambda is very limited: you can only have a single expression in the body of the lambda. That means no loops or complex statements, such as an if/else." ] }, { "cell_type": "code", "execution_count": null, "metadata": { "collapsed": false }, "outputs": [], "source": [ "factorial_cps(5, lambda v: v) ## call with initial continuation, the identity function" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Now we will have factorial return the continuation (rather than executing it right there) and use a trampoline." ] }, { "cell_type": "code", "execution_count": 82, "metadata": { "collapsed": false }, "outputs": [], "source": [ "def factorial_cps(n, k): ## add extra param, k for continuation\n", " if n == 1:\n", " return [k, 1] ## pass results to the continuation\n", " else:\n", " return factorial_cps(n - 1, ## move computation after call\n", " lambda v: [k, v * n]) ## to a new continuation passed in" ] }, { "cell_type": "code", "execution_count": 83, "metadata": { "collapsed": false }, "outputs": [ { "data": { "text/plain": [ "[>, 1]" ] }, "execution_count": 83, "metadata": {}, "output_type": "execute_result" } ], "source": [ "factorial_cps(5, lambda v: v)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "We can build a trampoline like we did in Scheme:" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "collapsed": false }, "outputs": [], "source": [ "def trampoline(result):\n", " while isinstance(result, list) and callable(result[0]):\n", " result = result[0](result[1])\n", " return result" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "And execute the code as we did in Scheme:" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "collapsed": false }, "outputs": [], "source": [ "trampoline(factorial_cps(5, lambda v: v))" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "However, **this still doesn't allow use to compute factorial of 1000.** \n", "\n", "This won't work yet:\n", "\n", "```python\n", "## DON'T DO THIS:\n", "trampoline(factorial_cps(1000, lambda v: v))\n", "```\n", "\n", "That is because we are using recursion to build up the chain of continuations. To fix this issue, we'll need one more step. But in practice, this will work fine for our evaluator.\n", "\n", "**Why?** Because we typically don't write code that will break the bounds of our recursive limits. For example, if we tried to write code like this:\n", "\n", "```scheme\n", "(+ (+ (+ (+ (+ (+ (+ .... repeat for 1000 times\n", "```\n", "\n", "Then our evaluator won't work. We _should_ allow code like that to work, but to do so will require one more step after CPS." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Convert Evaluator to CPS" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Now, we will tackle writing the evaluator into CPS. That will involve the same steps as before:\n", "\n", "1. add parameter k to the list of parameters\n", "2. if we have a result, give it to k (eg, \"apply the continuation\")\n", "3. if there is computation after the recursive call, create a continuation, and move the computation there\n", "4. always pass a continuation to the evaluator" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Here is the evaluator as it is:" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "collapsed": false }, "outputs": [], "source": [ "def evaluator(exp, env):\n", " if car(exp) == \"lit-exp\":\n", " return cadr(exp)\n", " elif car(exp) == \"var-exp\":\n", " return cadr(lookup_binding(cadr(exp), env))\n", " elif car(exp) == \"let-exp\":\n", " variable = cadr(exp)\n", " value = evaluator(caddr(exp), env)\n", " return evaluator(cadddr(exp), extend_env([variable], [value], env))\n", " elif car(exp) == \"if-exp\":\n", " if true_q(evaluator(cadr(exp), env)):\n", " return evaluator(caddr(exp), env)\n", " else:\n", " return evaluator(cadddr(exp), env)\n", " elif car(exp) == \"assign-exp\":\n", " variable = cadr(exp)\n", " value = evaluator(caddr(exp), env)\n", " binding = lookup_binding(variable, env)\n", " if binding:\n", " binding[1] = value\n", " else:\n", " raise Exception(\"No such variable: '%s'\" % variable)\n", " return value\n", " elif car(exp) == \"define-exp\":\n", " variable = cadr(exp)\n", " value = evaluator(caddr(exp), env)\n", " binding = lookup_binding(variable, env, True)\n", " if binding:\n", " binding[1] = value\n", " else:\n", " car(env).append([variable, value])\n", " return value\n", " elif car(exp) == \"procedure-exp\":\n", " return closure_exp(env, cadr(exp), caddr(exp))\n", " elif car(exp) == \"app-exp\":\n", " return applier(evaluator(cadr(exp), env), \n", " map(lambda e: evaluator(e, env), caddr(exp)))\n", " else:\n", " raise Exception(\"invalid abstract syntax: %s\" % exp)\n" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Below, `lit-exp`, `if-exp`, and `app-exp` have been converted for you. In addition, `evaluator1`, `evaluator2`, `evaluator3`, and `evaluator_map` have been written. Use `apply_cont` when you have a result.\n", "\n", "**Problem 1**: convert the rest of evaluator to CPS and test." ] }, { "cell_type": "code", "execution_count": null, "metadata": { "collapsed": true }, "outputs": [], "source": [ "def apply_cont(k, v):\n", " return k(v)" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "collapsed": false }, "outputs": [], "source": [ "def evaluator(exp, env):\n", " if car(exp) == \"lit-exp\":\n", " return apply_cont(k, cadr(exp))\n", " elif car(exp) == \"var-exp\":\n", " return cadr(lookup_binding(cadr(exp), env))\n", " elif car(exp) == \"let-exp\":\n", " variable = cadr(exp)\n", " value = evaluator(caddr(exp), env)\n", " return evaluator(cadddr(exp), extend_env([variable], [value], env))\n", " elif car(exp) == \"if-exp\":\n", " return evaluator(cadr(exp), env, lambda value: evaluator3(value, exp, env, k))\n", " elif car(exp) == \"assign-exp\":\n", " ## USE evaluator1 HERE!\n", " variable = cadr(exp)\n", " value = evaluator(caddr(exp), env)\n", " binding = lookup_binding(variable, env)\n", " if binding:\n", " binding[1] = value\n", " else:\n", " raise Exception(\"No such variable: '%s'\" % variable)\n", " return value\n", " elif car(exp) == \"define-exp\":\n", " ## USE evaluator2 HERE!\n", " variable = cadr(exp)\n", " value = evaluator(caddr(exp), env)\n", " binding = lookup_binding(variable, env, True)\n", " if binding:\n", " binding[1] = value\n", " else:\n", " car(env).append([variable, value])\n", " return value\n", " elif car(exp) == \"procedure-exp\":\n", " return closure_exp(env, cadr(exp), caddr(exp))\n", " elif car(exp) == \"app-exp\":\n", " return evaluator(cadr(exp), env, \n", " lambda v: evaluator_map(caddr(exp), env, \n", " lambda all: applier(v, all, k)))\n", " else:\n", " raise Exception(\"invalid abstract syntax: %s\" % exp)\n", "\n", "def evaluator1(value, variable, env, k):\n", " binding = lookup_binding(variable, env)\n", " if binding:\n", " binding[1] = value\n", " else:\n", " raise Exception(\"No such variable: '%s'\" % variable)\n", " return apply_cont(k, value)\n", "\n", "def evaluator2(value, variable, env, k):\n", " binding = lookup_binding(variable, env, True)\n", " if binding:\n", " binding[1] = value\n", " else:\n", " car(env).append([variable, value])\n", " return apply_cont(k, value) \n", " \n", "def evaluator3(value, exp, env, k):\n", " if true_q(value):\n", " return evaluator(caddr(exp), env, k)\n", " else:\n", " return evaluator(cadddr(exp), env, k)\n", "\n", "def evaluator_map(exp, env, k):\n", " if null_q(exp):\n", " return apply_cont(k, [])\n", " else:\n", " return evaluator_map(cdr(exp), env, \n", " lambda v1: evaluator(car(exp), env, \n", " lambda v2: apply_cont(k, cons(v2, v1))))" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Here is a CPS applier:" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "collapsed": false }, "outputs": [], "source": [ "def applier(f, args, k):\n", " if closure_q(f):\n", " env = cadr(f)\n", " parameters = caddr(f)\n", " body = cadddr(f)\n", " return evaluator(body, extend_env(parameters, args, env), k)\n", " else:\n", " return apply_cont(k, apply(f, args))" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "And finally, a convenience method:" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "collapsed": false }, "outputs": [], "source": [ "def calc(exp):\n", " return evaluator(parser(reader(lexer(exp))), toplevel_env, lambda v: v)" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "collapsed": false }, "outputs": [], "source": [ "calc(\"(define factorial (func (n) (if (= n 1) 1 (* (factorial (- n 1)) n))))\")" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "collapsed": false }, "outputs": [], "source": [ "calc(\"(factorial 5)\")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "That should now work. But it still calls the continuations from inside other continuations, getting deeper and deeper into our call stack.\n", "\n", "We can fix that by redefining our apply_continuation function:" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "collapsed": false }, "outputs": [], "source": [ "def apply_cont(k, v):\n", " return [k, v]" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "collapsed": false }, "outputs": [], "source": [ "calc(\"(factorial 5)\")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Of course, we again need a trampoline to execute the continuations:" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "collapsed": false }, "outputs": [], "source": [ "def continuation_q(exp):\n", " return isinstance(exp, list) and callable(exp[0])\n", "\n", "def trampoline(result):\n", " print result\n", " while continuation_q(result):\n", " result = car(result)(cadr(result))\n", " print result\n", " return result" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "collapsed": false }, "outputs": [], "source": [ "trampoline(calc(\"(+ 1 2)\"))" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "collapsed": false }, "outputs": [], "source": [ "trampoline(calc(\"(if (+ 1 2) 6 7)\"))" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "collapsed": false }, "outputs": [], "source": [ "trampoline(calc(\"((func (n) (+ n 1)) 42)\"))" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "collapsed": false }, "outputs": [], "source": [ "trampoline(calc(\"(factorial 5)\"))" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "If everything works correctly, we can now create infinite loops in our language implemented in Python, without crashing the call stack:" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "collapsed": false }, "outputs": [], "source": [ "calc(\"(loop)\")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Let's create a special trampoline (so we don't print out an infinite list of items):" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "collapsed": false }, "outputs": [], "source": [ "count = 0\n", "continuation = None\n", "def trampoline(result):\n", " global count, continuation\n", " while continuation_q(result):\n", " result = car(result)(cadr(result))\n", " count += 1\n", " if count % 1000 == 0:\n", " continuation = result\n", " return\n", " return result" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "We start it up, and run for 1000 steps:" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "collapsed": false }, "outputs": [], "source": [ "trampoline(calc(\"(loop)\"))" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "collapsed": false }, "outputs": [], "source": [ "count" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "We keep track of the last continuation so we can continue. Because the continuation is a result, we can just pass it back to the trampoline:" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "collapsed": false }, "outputs": [], "source": [ "trampoline(continuation)" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "collapsed": false }, "outputs": [], "source": [ "count" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "If we wanted, we could do that forever!" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "**Problem 2**: Make calc look as much like Scheme as you can. What else do you need to add to the language? Possible ideas: add strings, add `and` and `or`, add missing functions to the environment, add other special forms from Scheme, etc." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "**Problem 3**: Provide a summary description of what you have done in your own words. You should write as if you were explaining the process to someone who hasn't taken this course. It should explain continuations, CPS, and what problem we solved by using this technique. Also, demonstrate the additions that you made to the language. Use code to demonstrate where appropriate." ] } ], "metadata": { "kernelspec": { "display_name": "Python 2", "language": "", "name": "python2" }, "language_info": { "codemirror_mode": { "name": "ipython", "version": 2 }, "file_extension": ".py", "mimetype": "text/x-python", "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython2", "version": "2.7.6" } }, "nbformat": 4, "nbformat_minor": 0 }